# -*- coding: utf-8 -*-

from enum import Enum

from hlm.lane import LanePort, LanePortSender, LanePortReceiver, Lane


class ChannelPort:
    """ The base class of the Channel level input and output port.
    """
    def __init__(self, channel_type):
        """ Construct a base ChannelPort class.
        Args:
            channel_type: The type of the Channel class.
        """
        self.__channel_type = channel_type
        self.__lane_ports = []

    def add_port(self, port_name: str, port: LanePort) -> None:
        """ Add one lane level port to the current channel port.
        Args:
            port_name: The name of the port.
            port: The Lane port instance.
        """
        # Save the port name in list.
        self.__lane_ports.append(port_name)
        # Set the port name as attribute.
        setattr(self, port_name, port)

    def get_lane(self, lane_name: str, lane_type: type = None) -> LanePort:
        """ Get the lane port of the channel port.
        Args:
            lane_name: The name of the lane.
        Returns: The lane port instance.
        """
        return getattr(self, lane_name)

    def lane_ports(self) -> list:
        """ Get the lane port name list.
        Returns: A list of all the name of lanes.
        """
        return self.__lane_ports

    def channel(self):
        """ Get the current type of the channel.
        Returns: The current channel type.
        """
        return self.__channel_type


class ChannelPortReceiver(ChannelPort):
    """ The abstract class of one side of the ChannelPort. It contains all the
    ports on the value side of the lane map and the key side ports of the
    reverse map.
    This type of class will be automatically generated by the inheritance
    Channel class.
    """
    def __init__(self, channel_type):
        """ Create a ChannelPortIn instance. This is only a base class, which
        does the same as the ChannelPort.
        """
        super().__init__(channel_type)


class ChannelPortSender(ChannelPort):
    """ The abstract class of one side of the ChannelPort. It contains all the
    ports on the key side of the lane map and the value side ports of the
    reverse map.
    This type of class will be automatically generated by the inheritance Channel class.
    """
    def __init__(self, channel_type):
        """ Create a ChannelPortOut instance. This is only a base class, which
        does the same as the ChannelPort.
        """
        super().__init__(channel_type)


class ChannelProperty(Enum):
    """ ChannelProperty provides the properties type of the lane map.
    """
    PORTS = 1
    REVERSE = 2
    GET = 3


class Channel:
    """ Channel class is a wrapper around multiple lanes. It is understood as a
    collection of lanes from one node to another. A channel constitutes of lanes
    that do not necessarily send data in the same direction.
    To implement a new Channel, start by subclassing the Channel class. Then set
    the `lanes_map` dictionary. This is a class level variable, shares between
    the same level class. In the `lanes_map`, it should be in the following
    format::
        lanes_map = {(The name of Lane Type):
                        {ChannelProperty.PORTS: (The same type like ports of Lane class),
                         ChannelProperty.REVERSE: (Optional, set this value to True when the sender and receiver side is
                                                   reversed)
                         ChannelProperty.GET: (Optional, the customized get order)
                     ...}
    Basically, the `GET` operations of each Lane is needed to be implemented by
    user. It must be a list of strings. For example::
        ChannelProperty.GET: ['addr', 'enable', 'data']
    """
    lanes_map = {}

    @staticmethod
    def is_reverse(item_params: dict) -> bool:
        """ Check whether the mapping dictionary of Lane is reversed.
        Args:
            item_params: The Lane generated information dictionary.
        Returns: If the dictionary contains `ChannelProperty.REVERSE` key and
        the value is set to True, return True, otherwise False.
        """
        return ChannelProperty.REVERSE in item_params and \
               item_params[ChannelProperty.REVERSE]

    @staticmethod
    def generate_lane_receiver(lane_dict: dict) -> LanePortReceiver:
        """ Generate a Lane receiver port from the lane dictionary of the
        channel.
        Args:
            lane_dict: The lane port define dictionary.
        """
        return LanePortReceiver(
            lane_dict[ChannelProperty.PORTS],
            lane_dict[ChannelProperty.GET]
            if ChannelProperty.GET in lane_dict else [])

    @staticmethod
    def generate_lane_sender(lane_dict: dict) -> LanePortSender:
        """ Generate a Lane sender port from the lane dictionary of the channel.
        Args:
            lane_dict: The lane port define dictionary.
        """
        return LanePortSender(
            Lane.get_port_name_list(lane_dict[ChannelProperty.PORTS]))

    @classmethod
    def get_receiver(cls) -> ChannelPortReceiver:
        """ Get the channel receiver port.
        Returns: The channel receiver port.
        """
        # Create an empty receiver.
        receiver = ChannelPortReceiver(cls)
        # Based on the lane map, create the lane ports.
        for lane_name in cls.lanes_map:
            # Based on the reverse detection, create different lane port.
            lane_dict = cls.lanes_map[lane_name]
            if Channel.is_reverse(lane_dict):
                # On receiver side, a reversed lane should use LanePortSender.
                receiver.add_port(lane_name,
                                  Channel.generate_lane_sender(lane_dict))
            else:
                # For normal situation, it should be a LanePortReceiver.
                receiver.add_port(lane_name,
                                  Channel.generate_lane_receiver(lane_dict))
        return receiver

    @classmethod
    def get_sender(cls) -> ChannelPortSender:
        """ Get a channel sender port.
        Returns: The channel sender port.
        """
        # Create an empty sender.
        sender = ChannelPortSender(cls)
        # Based on the lane map, create the lane ports.
        for lane_name in cls.lanes_map:
            # Based on the reverse detection, create different lane port.
            lane_dict = cls.lanes_map[lane_name]
            if Channel.is_reverse(lane_dict):
                # On the receiver side, a reversed lane should use
                # LanePortReceiver.
                sender.add_port(lane_name,
                                Channel.generate_lane_receiver(lane_dict))
            else:
                # For normal situation, it should be a LanePortSender.
                sender.add_port(lane_name,
                                Channel.generate_lane_sender(lane_dict))
        return sender

    @staticmethod
    def connect_ports(lanes_map: dict,
                      sender: ChannelPortSender,
                      receiver: ChannelPortReceiver) -> None:
        """ Connect the channel level port by providing the lane map of the
        channel and ports need to be connected.
        Args:
            lanes_map: The map of the lanes port for connection.
            sender: The channel sender port.
            receiver: The channel receiver port.
        """
        # Check the type of the ports.
        if receiver.channel() != sender.channel():
            raise Exception('The type of sender {0} and '
                            'receiver {1} mismatch.'.format(
                str(sender.channel()), str(receiver.channel())))
        # For each lane, connect them one by one.
        for lane_name in lanes_map:
            # Get the port from the channel port.
            port_on_sender = getattr(sender, lane_name)
            port_on_receiver = getattr(receiver, lane_name)
            # Get the channel dictionary.
            lane_dict = lanes_map[lane_name]
            # Check reverse.
            if Channel.is_reverse(lane_dict):
                # Connect ports.
                Lane.connect_ports(lane_dict[ChannelProperty.PORTS],
                                   port_on_receiver, port_on_sender)
            else:
                # Connect normally.
                Lane.connect_ports(lane_dict[ChannelProperty.PORTS],
                                   port_on_sender, port_on_receiver)

    @classmethod
    def connect(cls,
                sender: ChannelPortSender,
                receiver: ChannelPortReceiver) -> None:
        # Connect the ports.
        Channel.connect_ports(cls.lanes_map, sender, receiver)